﻿/*
  KeePass Password Safe - The Open-Source Password Manager
  Copyright (C) 2003-2017 Dominik Reichl <dominik.reichl@t-online.de>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;

using KeePassLib.Cryptography;
using KeePassLib.Resources;
using KeePassLib.Utility;

namespace KeePassLib.Serialization
{
  public sealed class FileLockException : Exception
  {
    private readonly string m_strMsg;

    public override string Message
    {
      get { return m_strMsg; }
    }

    public FileLockException(string strBaseFile, string strUser)
    {
      StringBuilder sb = new StringBuilder();

      if (!string.IsNullOrEmpty(strBaseFile))
      {
        sb.Append(strBaseFile);
        sb.Append(MessageService.NewParagraph);
      }

      sb.Append(KLRes.FileLockedWrite);
      sb.Append(MessageService.NewLine);

      if (!string.IsNullOrEmpty(strUser)) sb.Append(strUser);
      else sb.Append("?");

      sb.Append(MessageService.NewParagraph);
      sb.Append(KLRes.TryAgainSecs);

      m_strMsg = sb.ToString();
    }
  }

  public sealed class FileLock : IDisposable
  {
    private const string LockFileExt = ".lock";
    private const string LockFileHeader = "KeePass Lock File";

    private IOConnectionInfo m_iocLockFile;

    private sealed class LockFileInfo
    {
      public readonly string ID;
      public readonly DateTime Time;
      public readonly string UserName;
      public readonly string Machine;
      public readonly string Domain;

      private LockFileInfo(string strID, string strTime, string strUserName,
          string strMachine, string strDomain)
      {
        this.ID = (strID ?? string.Empty).Trim();

        DateTime dt;
        if (TimeUtil.TryDeserializeUtc(strTime.Trim(), out dt))
          this.Time = dt;
        else
        {
          Debug.Assert(false);
          this.Time = DateTime.UtcNow;
        }

        this.UserName = (strUserName ?? string.Empty).Trim();
        this.Machine = (strMachine ?? string.Empty).Trim();
        this.Domain = (strDomain ?? string.Empty).Trim();

        if (this.Domain.Equals(this.Machine, StrUtil.CaseIgnoreCmp))
          this.Domain = string.Empty;
      }

      public string GetOwner()
      {
        StringBuilder sb = new StringBuilder();
        sb.Append((this.UserName.Length > 0) ? this.UserName : "?");

        bool bMachine = (this.Machine.Length > 0);
        bool bDomain = (this.Domain.Length > 0);
        if (bMachine || bDomain)
        {
          sb.Append(" (");
          sb.Append(this.Machine);
          if (bMachine && bDomain) sb.Append(" @ ");
          sb.Append(this.Domain);
          sb.Append(")");
        }

        return sb.ToString();
      }

      public static LockFileInfo Load(IOConnectionInfo iocLockFile)
      {
        Stream s = null;
        try
        {
          s = IOConnection.OpenRead(iocLockFile);
          if (s == null) return null;
          StreamReader sr = new StreamReader(s, StrUtil.Utf8);
          string str = sr.ReadToEnd();
          sr.Close();
          if (str == null) { Debug.Assert(false); return null; }

          str = StrUtil.NormalizeNewLines(str, false);
          string[] v = str.Split('\n');
          if ((v == null) || (v.Length < 6)) { Debug.Assert(false); return null; }

          if (!v[0].StartsWith(LockFileHeader)) { Debug.Assert(false); return null; }
          return new LockFileInfo(v[1], v[2], v[3], v[4], v[5]);
        }
        catch (FileNotFoundException) { }
        catch (Exception) { Debug.Assert(false); }
        finally { if (s != null) s.Close(); }

        return null;
      }

      // Throws on error
      public static LockFileInfo Create(IOConnectionInfo iocLockFile)
      {
        LockFileInfo lfi;
        Stream s = null;
        try
        {
          byte[] pbID = CryptoRandom.Instance.GetRandomBytes(16);
          string strTime = TimeUtil.SerializeUtc(DateTime.UtcNow);

          lfi = new LockFileInfo(Convert.ToBase64String(pbID), strTime,
#if KeePassUAP
						EnvironmentExt.UserName, EnvironmentExt.MachineName,
						EnvironmentExt.UserDomainName);
#elif KeePassLibSD
						string.Empty, string.Empty, string.Empty);
#else
              Environment.UserName, Environment.MachineName,
              Environment.UserDomainName);
#endif

          StringBuilder sb = new StringBuilder();
#if !KeePassLibSD
          sb.AppendLine(LockFileHeader);
          sb.AppendLine(lfi.ID);
          sb.AppendLine(strTime);
          sb.AppendLine(lfi.UserName);
          sb.AppendLine(lfi.Machine);
          sb.AppendLine(lfi.Domain);
#else
					sb.Append(LockFileHeader + MessageService.NewLine);
					sb.Append(lfi.ID + MessageService.NewLine);
					sb.Append(strTime + MessageService.NewLine);
					sb.Append(lfi.UserName + MessageService.NewLine);
					sb.Append(lfi.Machine + MessageService.NewLine);
					sb.Append(lfi.Domain + MessageService.NewLine);
#endif

          byte[] pbFile = StrUtil.Utf8.GetBytes(sb.ToString());

          s = IOConnection.OpenWrite(iocLockFile);
          if (s == null) throw new IOException(UrlUtil.GetFileName(iocLockFile.Path));
          s.Write(pbFile, 0, pbFile.Length);
        }
        finally { if (s != null) s.Close(); }

        return lfi;
      }
    }

    public FileLock(IOConnectionInfo iocBaseFile)
    {
      if (iocBaseFile == null) throw new ArgumentNullException("strBaseFile");

      m_iocLockFile = iocBaseFile.CloneDeep();
      m_iocLockFile.Path += LockFileExt;

      LockFileInfo lfiEx = LockFileInfo.Load(m_iocLockFile);
      if (lfiEx != null)
      {
        m_iocLockFile = null; // Otherwise Dispose deletes the existing one
        throw new FileLockException(UrlUtil.GetFileName(iocBaseFile.Path), lfiEx.GetOwner());
      }

      LockFileInfo.Create(m_iocLockFile);
    }

    ~FileLock()
    {
      Dispose(false);
    }

    public void Dispose()
    {
      Dispose(true);
      GC.SuppressFinalize(this);
    }

    private void Dispose(bool bDisposing)
    {
      if (m_iocLockFile == null) return;

      bool bFileDeleted = false;
      for (int r = 0; r < 5; ++r)
      {
        // if(!OwnLockFile()) { bFileDeleted = true; break; }

        try
        {
          IOConnection.DeleteFile(m_iocLockFile);
          bFileDeleted = true;
        }
        catch (Exception) { Debug.Assert(false); }

        if (bFileDeleted) break;

        if (bDisposing) Thread.Sleep(50);
      }

      // if(bDisposing && !bFileDeleted)
      //	IOConnection.DeleteFile(m_iocLockFile); // Possibly with exception

      m_iocLockFile = null;
    }

    // private bool OwnLockFile()
    // {
    //	if(m_iocLockFile == null) { Debug.Assert(false); return false; }
    //	if(m_strLockID == null) { Debug.Assert(false); return false; }
    //	LockFileInfo lfi = LockFileInfo.Load(m_iocLockFile);
    //	if(lfi == null) return false;
    //	return m_strLockID.Equals(lfi.ID);
    // }
  }
}
